Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.

...powered by www.netzwerkartist.de...

 << zurück
Visual C# 2005 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2005

Visual C# 2005
1.320 S., mit 2 CDs, 59,90 Euro
Galileo Computing
ISBN 3-89842-586-X
gp Kapitel 7 Weitere Möglichkeiten von C#
  gp 7.1 Operatorüberladung
    gp 7.1.1 Die Syntax der Operatorüberladung
    gp 7.1.2 Beispiel einer Operatorüberladung
    gp 7.1.3 Überladungsbeispiele
    gp 7.1.4 Benutzerdefinierte Konvertierungen – implizit und explizit
  gp 7.2 Indexer
    gp 7.2.1 Überladen von Indexern
    gp 7.2.2 Parameterbehaftete Eigenschaften
  gp 7.3 Collections (Auflistungen)
    gp 7.3.1 Die elementaren Schnittstellen der Auflistungsklassen
    gp 7.3.2 Die Klasse »ArrayList«
    gp 7.3.3 Das Sortieren der Elemente einer »ArrayList«
    gp 7.3.4 Die Schnittstelle »IDictionary«
    gp 7.3.5 Die Klasse »Hashtable«
    gp 7.3.6 Die Klassen »Queue« und »Stack«
    gp 7.3.7 Objektauflistungen im Überblick
    gp 7.3.8 Benutzerdefinierte Auflistungen
  gp 7.4 Generics – Generische Datentypen
    gp 7.4.1 Die Typproblematik am Beispiel der Klasse »Stack«
    gp 7.4.2 Die Lösung mit einer generischen Klasse
    gp 7.4.3 Typparameter mit Constraints einschränken
    gp 7.4.4 Generische Methoden
    gp 7.4.5 Generics und Vererbung
    gp 7.4.6 Konvertierung von Generics
    gp 7.4.7 Generische Delegate
    gp 7.4.8 Generische Klassen in der .NET-Klassenbibliothek
    gp 7.4.9 Eigene Auflistungen mit »yield« durchlaufen
    gp 7.4.10 Daten durch »null« beschreiben
  gp 7.5 Fortgeschrittene Delegat-Techniken
    gp 7.5.1 Multicast-Delegate
  gp 7.6 Attribute
    gp 7.6.1 Das »Flags«-Attribut
    gp 7.6.2 Anmerkungen zu den Attributen
    gp 7.6.3 Benutzerdefinierte Attribute
  gp 7.7 Unsicherer Programmcode – Zeigertechnik in C#
    gp 7.7.1 Das Schlüsselwort »unsafe«
    gp 7.7.2 Die Deklaration von Zeigern
    gp 7.7.3 Die »fixed«-Anweisung
    gp 7.7.4 Zeigerarithmetik
    gp 7.7.5 Der Operator »->«


Galileo Computing

7.3 Collections (Auflistungendowntop

Ein wesentliches charakteristisches Merkmal der Arrays ist die freie Verfügbarkeit ihrer Indizes. Sie können ein Element einem Array an einer x-beliebigen Position hinzufügen – unabhängig davon, ob der Index bereits von einem anderen Element belegt ist oder nicht. Wird ein Element aus einem Array entfernt, bleibt ein unbesetzter Index zurück. Ein Array ist somit ein statischer Pool freier und belegter Elementpositionen ohne die Fähigkeit, sich an Änderungen dynamisch anpassen zu können.

An dieser Stelle treten Klassen in Erscheinung, die – ähnlich den Arrays – als Container meist typgleicher Elemente dienen. Im Unterschied zu den herkömmlichen Arrays arbeiten diese Klassen jedoch dynamisch: Sie enthalten keine unbelegten Indizes, sondern vergrößern oder verkleinern ihre Kapazität entsprechend der Anzahl der Einträge. Ganz allgemein werden diese Klassen als Collections oder auch Auflistungen bezeichnet. Jede Klasse unterscheidet sich von der anderen durch besondere Fähigkeiten – sei es die interne Verwaltung der Objekte, der Zugriff auf die Einträge oder die Geschwindigkeit, mit der innerhalb einer Liste nach einem bestimmten Eintrag gesucht werden kann.

Damit Sie die Namen der wichtigsten Auflistungen, mit denen wir uns auch auf den folgenden Seiten beschäftigen werden, schon einmal gehört haben, seien sie hier aufgeführt:

gp  ArrayList
gp  Hashtable
gp  Queue
gp  Stack

Diese Klassen, die alle zum Namespace System.Collections gehören, unterscheiden sich in den Methoden, mit denen der Zugriff auf die Elemente erfolgt, wie die Elemente im Speicher verwaltet werden und welche Operationen sich darauf ausführen lassen. Jede Klasse hat ihre eigene Charakteristik.


Galileo Computing

7.3.1 Die elementaren Schnittstellen der Auflistungsklassen  downtop

Die Grundfunktionalität aller Auflistungen lässt sich auf elementare Methoden zurückführen. Es ist deshalb nicht verwunderlich, dass die Gemeinsamkeiten durch Schnittstellen beschrieben werden, die von den Klassen implementiert werden. Dabei handelt es sich um die folgenden:

gp  IEnumerable
gp  ICollection
gp  IDictionary
gp  IList

Diese Schnittstellen bilden eine Hierarchie, deren Wurzel IEnumerable ist, aus der ICollection abgeleitet wird. Beide Schnittstellen sind charakteristisch für Auflistungsklassen, denn sie stellen die wichtigsten Grundfunktionalitäten bereit. IDictionary und IList leiten sich zudem aus ICollection ab und spalten die Auflistungsklassen in zwei Gruppen:

1.  Klassen, die das Interface IList implementieren, beschreiben Objektauflistungen, auf deren Einträge über einen Index zugegriffen wird.
2. Klassen, die das Interface IDictionary implementieren, verwalten ihre Einträge über eine Schlüssel-Wert-Kombination.
       

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.1   Die Schnittstellen der Auflistungsklassen

Die Schnittstelle »IEnumerable«

Diese Schnittstelle hat nur die Methode GetEnumerator, die ein Enumerator-Objekt bereitstellt. Ein Enumerator verfügt über die Fähigkeit, eine Auflistung elementweise zu durchlaufen. Damit gleicht dieses Objekt einem Positionszeiger, dem drei Methoden eigen sind: Current, MoveNext und Reset.

Der Enumerator positioniert sich standardmäßig vor dem ersten Eintrag einer Auflistung. Um ihn auf den ersten Eintrag und anschließend auf alle Folgeeinträge zeigen zu lassen, muss die Methode MoveNext ausgeführt werden. Mit Current wird auf den Eintrag zugegriffen, auf den der Enumerator aktuell zeigt. Reset setzt den Enumerator in seine Ausgangsposition zurück, also vor den ersten Eintrag.

Es gibt eine Situation, in der die Fähigkeit des Enumerators ausgesprochen wichtig ist. Es ist die foreach-Schleife, mit der eine Auflistung vom ersten bis zum letzten Element durchlaufen wird:


foreach(Typ Variable in Auflistung) {
  // Anweisungen
}

Die Schnittstelle »ICollection«

Die Schnittstelle ICollection stattet alle Auflistungen mit weiteren Fähigkeiten aus. Diese Schnittstelle veröffentlicht die in der folgenden Tabelle aufgeführten Eigenschaften und Methoden.


Tabelle 7.2   Mitglieder der Schnittstelle »ICollection«

Eigenschaft/Methode Beschreibung
Count Liefert die Anzahl der Elemente einer Auflistung.
IsSynchronized Liefert einen booleschen Wert, der darüber Auskunft gibt, ob das Auflistungsobjekt synchronisiert, also thread-sicher ist.
SyncRoot Liefert eine Objektreferenz, die den Zugriff auf das angegebene Objekt synchronisiert.
CopyTo Kopiert die Elemente einer Auflistung in ein Array.

Insbesondere Auflistungen sind kritisch hinsichtlich des gleichzeitigen Zugriffs mehrerer Threads. Viele Klassen implementieren Thread-Sicherheit durch die Bereitstellung der Methode Synchronized, beispielsweise auch die Klassen ArrayList und Hashtable. Die Eigenschaft IsSynchronized gibt an, ob die Auflistung synchronisiert ist. Weitere Informationen zu Threads, der Synchronisierung und der Eigenschaft SyncRoot erhalten Sie in Kapitel 11.

Die Schnittstelle »IList«

Auflistungen, die IList implementieren, zeichnen sich dadurch aus, ihre Elemente über Indizes verwalten zu können. Das beste Beispiel hierfür dürfte die Klasse ArrayList sein, aber auch eine große Anzahl weiterer, meist steuerelementspezifischer Auflistungen gehört zu dieser Gruppe.

Wir wollen uns daher zuerst die wichtigsten Eigenschaften und Methoden ansehen, die alle Klassen, die IList implementieren, gemeinsam aufweisen.


Tabelle 7.3   Methoden und Eigenschaften der Schnittstelle »IList«

Methode/Eigenschaft Beschreibung
Item Stellt den Indexer für die IList-Klasse dar und dient dem Zugriff auf ein Listenelement.
Add Hängt ein neues Element an das Ende der Auflistung an.
Clear Löscht alle Elemente der Auflistung.
Contains Gibt zurück, ob ein bestimmtes Objekt bereits zu der Auflistung gehört.
IndexOf Liefert den Index eines bestimmten Objekts.
Insert Fügt ein Objekt an einer bestimmten Position in die Auflistung ein.
IsFixedSize Beschreibt mit einem booleschen Wert, ob die Kapazität der Auflistung dynamisch vergrößert werden kann oder nicht.
IsReadOnly Beschreibt mit einem booleschen Wert, ob die Auflistung schreibgeschützt ist.
Remove Löscht ein Objekt unter der Angabe der Referenz.
RemoveAt Löscht ein Objekt unter der Angabe des Index.

Ihre Stärke spielen Schnittstellen aus, wenn sie von mehreren Klassen implementiert werden. Jede Klasse weist dann dieselben Merkmale und Verhaltensweisen auf. Wenn Sie mit einer Klasse gearbeitet haben, die eine oder mehrere gängige Schnittstellen unterstützt, sollten Sie auch mit allen anderen Klassen umgehen können, welche die gleichen Schnittstellen unterstützen. Das trifft insbesondere auf die Schnittstelle IList zu, weil sie von sehr vielen Klassen des .NET Frameworks implementiert wird. Es ist daher empfehlenswert, sich insbesondere mit den Eigenschaften und Methoden dieser Schnittstelle vertraut zu machen. Beispiele dazu werden Ihnen im weiteren Verlauf dieses Buchs noch ausgesprochen viele begegnen.


Galileo Computing

7.3.2 Die Klasse »ArrayList«  downtop

ArrayList ist die wichtigste Klasse, die das Interface IList implementiert. Ein Objekt vom Typ ArrayList hat standardmäßig eine Kapazität von 16 Einträgen, die sich bei Überschreitung automatisch verdoppelt. Die Kapazität kann beim Erzeugen des Objekts über einen der überladenen Konstruktoren festgelegt werden oder durch Zuweisung an die Eigenschaft Capacity.

Einträge hinzufügen

Mit der aus der Schnittstelle IList geerbten Methode Add können Objekte einer ArrayList-Instanz hinzugefügt werden. Da ArrayList eine 0-basierte Auflistung ist, wird der erste Eintrag den Index 0 haben, das zweite den Index 1 usw. Sie haben mit der Add-Methode keinen Einfluss darauf, an welcher Position das Objekt in der Liste aufgenommen wird, denn es wird an das Listenende gesetzt. Wollen Sie wissen, welchen Index ein hinzugefügtes Objekt erhalten hat, brauchen Sie nur den Rückgabewert der Add-Methode auszuwerten.

ArrayList liste = new ArrayList();
int index = liste.Add(irgendeinObjekt);

Über die von IList übernommene Methode Add hinaus bietet ArrayList mit AddRange eine weitere, typspezifische Methode an, der Sie auch herkömmliche Arrays übergeben können:


ArrayList arr = new ArrayList();
int[] intArr = {0, 10, 22, 9, 45};
arr.AddRange(intArr);

Liegt das Array bereits vor der Instanziierung von ArrayList vor, kann das Array auch direkt dem Konstruktor übergeben werden:


ArrayList arr = new ArrayList(intArr);

Die Elementverwaltung einer »ArrayList«

Nehmen wir an, mehrere Objekte vom Typ ClassA sollen von einer ArrayList verwaltet werden. ClassA sei wie folgt definiert:


public class ClassA {
  public int Prop;
  public ClassA(int x) {
    Prop = x;
}

Im folgenden Code werden die beiden Referenzen obj1 und obj2 vom Typ ClassA der Auflistung col hinzugefügt. Die Auflistung col verwaltet danach obj1 über den Index 0 und obj2 über den Index 1.


ClassA obj1 = new ClassA(1);
ClassA obj2 = new ClassA(2);
// Klasse ArrayList instanziieren
ArrayList col = new ArrayList();
// obj1 und obj2 der Auflistung hinzufügen
col.Add(obj1);
col.Add(obj2);

Damit hätten wir aktuell die folgenden Zuordnungen:

gp  Der Listeneintrag col[0] enthält die Referenz obj1.
gp  Der Listeneintrag col[1] enthält die Referenz obj2.

Mit Add haben Sie keinen Einfluss auf die Positionierung der Objekte innerhalb der Liste. Wollen Sie ein Objekt jedoch an einer bestimmten Position einsortieren, sollten Sie anstelle der Add-Methode die Methode Insert benutzen:


ClassA obj3 = new ClassA(3);
col.Insert(1, obj3);

Das über obj3 referenzierte Objekt wird damit unter dem Index 1 registriert. Falls man versucht, einen Index anzugeben, der größer ist als die Anzahl der Elemente in der Auflistung, kommt es zur Ausnahme ArgumentOutOfRangeException, da der letzte verfügbare Index immer genauso groß ist, wie die Auflistung Elemente enthält.

Was passiert aber mit dem Element obj2, das vorher die Position des Index 1 einnahm? Wir können das prüfen, indem wir beispielsweise eine Methode schreiben, der wir die Referenz auf die Auflistung als Argument übergeben. In der Methode werden die Elemente der übergebenen Auflistung vom ersten bis zum letzten Objekt an der Konsole ausgegeben:


// Auflistung enthält nur typgleiche Einträge
public void GetListElements(IList list) {
  foreach(ClassA temp in list) {
    Console.Write("Collection-Index = {0}", list.IndexOf(temp));
    Console.WriteLine(" / Objekt-Nr.{0}", temp.intProp);
  }
}

Der Typ des Parameters ist IList. Wir hätten auch den Typ ArrayList wählen können, halten uns aber mit unserer Festlegung allgemeiner und haben damit eine Methode, die jeden beliebigen Auflistungstyp entgegennimmt, der IList implementiert und ClassA-Objekte verwaltet.

In der Methode GetListElements wird in einer foreach-Schleife die Auflistung vom ersten bis zum letzten Element durchlaufen. Die Laufvariable temp ist vom Typ ClassA deklariert, da wir wissen, dass unsere Auflistung nur Objekte dieses Typs enthält.

Typgleiche Objekte in einer der Auflistungsklassen zu verwalten, ist die Regel. Der Grund dafür ist einleuchtend, denn innerhalb der Schleife wird häufig ein typspezifisches Member des sich aktuell im Zugriff befindlichen Objekts aufgerufen. Verwaltet eine Auflistung unterschiedliche Typen, muss die Laufvariable der Schleife allgemeiner typisiert werden. Innerhalb des Schleifenblocks sind dann eine Typüberprüfung sowie eine Typumwandlung erforderlich, um auf ein spezifisches Merkmal des Objekts zuzugreifen. Das geht natürlich zu Lasten der Performance.


// Auflistung enthält unterschiedliche Typen
public static void GetListElements(IList list) {
  foreach(object temp in list) {
    if(temp is ClassA) {
      Console.Write("Collection-Index = {0}", list.IndexOf(temp));
      Console.WriteLine(" / Objekt-Nr.{0}", ((ClassA)temp).intProp);
    }
  }
}

Sehen wir uns nun die Ausgabe an, die von der Prozedur GetListElements in das Konsolenfenster geschrieben wird, nachdem wir mit Insert ein drittes Objekt an die zweite Listenposition gesetzt haben:


Collection-Index = 0 / Objekt-Nr.1
Collection-Index = 1 / Objekt-Nr.3
Collection-Index = 2 / Objekt-Nr.2

Das ursprünglich dem Index 1 zugeordnete Objekt musste seine Position räumen – es verschiebt sich in der Liste um eine Position in Richtung Listenende, während sich obj3 wunschgemäß einreiht. Hätten wir noch weitere Elemente in unserer Auflistung, würde sich die Indizierung aller Folgeelemente ebenfalls um eine Position verschieben.

Ein ähnliches Verhalten zeigt sich auch, wenn wir mit Remove oder RemoveAt aus der Auflistung einen Eintrag löschen: Der freigegebene Index bleibt nicht unbelegt, sondern bewirkt eine Indexverschiebung aller Nachfolgeelemente. Löschen wir beispielsweise das Element mit dem Index 2, rutscht das Objekt mit dem ursprünglichen Index 3 an die frei gewordenen Position 2, das Objekt mit dem Index 4 füllt die Lücke des Index 3 usw. (siehe auch Abbildung 7.2).

Die Tragweite dieser Elementverwaltung ist weitreichend, denn es kann zu keinem Zeitpunkt die eindeutige Zuordnung eines Objekts zu einem bestimmten Index in der Auflistung garantiert werden.

Auf der Buch-CD finden Sie den Programmcode des Beispiels unter:

...\Kapitel 7\ArrayListDemo

Abbildung
Hier klicken, um das Bild zu vergrößern

Abbildung 7.2   Listenverwaltung beim Löschen

Datenaustausch zwischen einem Array und einer »ArrayList«

Auflistungen zeichnen sich durch die beiden Interfaces IEnumerable und ICollection aus. Aus der letztgenannten stammt die Methode CopyTo, die es ermöglicht, die Einträge einer Auflistung in ein Array zu kopieren.


ArrayList col = new ArrayList();
col.Add("Anton");
col.Add("Gustaf");
col.Add("Fritz");
string[] strArr = new string[10];
col.CopyTo(strArr, 3);

Der zweite Parameter von CopyTo gibt den Startindex im Array an, ab dem kopiert wird. Das Array muss groß genug sein, um alle Elemente aufzunehmen, sonst wird ein Fehler ausgelöst. Handelt es sich bei den zu kopierenden Einträgen um Objektreferenzen, werden nicht die Objekte, sondern nur die Referenzen kopiert. ArrayList überlädt CopyTo, so dass auch spezifizierte Teilbereiche der Liste kopiert werden können.


Galileo Computing

7.3.3 Das Sortieren der Elemente einer »ArrayList«  downtop

Die von ArrayList verwalteten Objekte sind sortierbar. Das ist keineswegs eine Selbstverständlichkeit, sondern ein wichtiges Charakteristikum dieser Klasse, wie wir später noch im Vergleich mit anderen Auflistungen feststellen werden.

Sortieren mit der Schnittstelle »IComparable«

Um die Mitglieder zu sortieren, wird die Methode Sort auf der ArrayList-Referenz aufgerufen. Sort ist mehrfach überladen. Wir wollen uns zunächst mit der parameterlosen Version beschäftigen:


public virtual void Sort();

Die Regel, nach der im deutsprachigen Raum sortiert wird, vergleicht die Zeichen unter Berücksichtigung der Groß- und Kleinschreibung wie folgt:


1 < 2 ... < a < A < b < B < c < C ... < y < Y < z < Z

Um die verwalteten Objekte einer ArrayList mit der parameterlosen Sort-Methode zu sortieren, müssen die Objekte die Schnittstelle IComparable implementieren. Diese Schnittstelle enthält nur die Methode CompareTo:


public interface IComparable {
  int CompareTo(object obj);
}

Eine Klasse, die IComparable implementiert, garantiert, die Methode CompareTo zu veröffentlichen. Darauf ist die Sort-Methode der ArrayList angewiesen. Was eine Schnittstellenmethode leisten muss, ist der jeweiligen Dokumentation zu entnehmen. Aus der .NET-Dokumentation zu CompareTo können wir entnehmen, dass das aktuelle Objekt mit dem des Parameters verglichen wird. Als Resultat liefert der Methodenaufruf einen der drei folgenden Werte:

gp  < 0, wenn das aktuelle Objekt »kleiner« als das Objekt obj ist.
gp  0, wenn das aktuelle Objekt »gleich« dem Objekt obj ist.
gp  > 0, wenn das aktuelle Objekt »größer« als das Objekt obj ist.

Die Kriterien, was im Vergleich als »kleiner«, »gleich« und »größer« bewertet wird, muss die Klasse festlegen, welche die Schnittstelle IComparable implementiert. Sehen wir uns dazu ein Beispiel an:


public class HoldValue : IComparable {
  public int intVar;
  public HoldValue(int x) {
    intVar = x;
  }
  public int CompareTo(object obj) {
    HoldValue val = (HoldValue)obj;
    if(val.intVar < this.intVar)
      return 1;
    else if(val.intVar == this.intVar)
      return 0;
    else 
      return –1;
  }
}

Die Klasse HoldValue implementiert IComparable. Daher sind Objekte dieses Typs darauf vorbereitet, in einer ArrayList sortiert zu werden. Die Sortierreihenfolge soll sich am Inhalt des Felds intVar orientieren. In der Methode CompareTo wird die dem Parameter übergebene Referenz zuerst in den Typ HoldValue konvertiert und einer lokalen Variablen zugewiesen. Anschließend folgt ein Vergleich zwischen den Feldwerten des aktuellen und des übergebenen Objekts.

Natürlich wollen wir nun auch testen, ob wir unser Ziel erreicht haben. Dazu dient der folgende Testcode:


static void Main(string[] args) {
  ArrayList arrList = new ArrayList();
  HoldValue obj1 = new HoldValue(17);
  arrList.Add(obj1);
  HoldValue obj2 = new HoldValue(110);
  arrList.Add(obj2);
  HoldValue obj3 = new HoldValue(5);
  arrList.Add(obj3);
  arrList.Sort();
  int i = 0;
  foreach(HoldValue temp in arrList) {
    Console.WriteLine("Element{0} – Wert: {1}", i, temp.intVar);
    i++;
  }
}

An der Konsole werden die Werte der Felder in der Reihenfolge 5, 17, 100 ausgegeben, obwohl die ursprüngliche Reihenfolge in der Liste eine andere war. Der Vergleich und die anschließende Sortierung finden also wie erwartet statt.

Der Code lässt sich aber auch noch eleganter formulieren. Wenn Sie sich in der .NET-Dokumentation die Definition der Struktur Int32 ansehen, werden Sie feststellen, dass dieser Typ seinerseits selbst die Schnittstelle IComparable implementiert. Es ist daher nahe liegend, den Vergleich am Feld intVar direkt vorzunehmen:


public int CompareTo(object obj) {
  HoldValue val = (HoldValue)obj;
  return this.intVar.CompareTo(val.intVar);
}

Damit können wir uns aber noch nicht ganz zufrieden geben, denn alle denkbaren Szenarien werden von unserer Implementierung noch nicht berücksichtigt. Es könnte nämlich auch ein Objekt übergeben werden, dass mit dem aktuellen nicht vergleichbar ist, beispielsweise:


Circle kreis = new Circle(5);
HoldValue val = new HoldValue(8);
int x = val.CompareTo(kreis);

Wenn Sie die Methode CompareTo implementieren, sollten Sie diesen Fall ebenso berücksichtigen wie die Eventualität, dass das übergebene Objekt noch nicht initialisiert und daher null ist. Die Implementierung, die diese beiden Szenarien einbezieht, sieht wie folgt aus:


public int CompareTo(object obj) {
  // prüfen, ob der Parameter ein null-Verweis ist
  if(obj == null)
    return 1;
  // prüfen, ob beide Typen gleich sind
  if(obj.GetType() != this.GetType())
    throw new ArgumentException("Ungültiger Vergleich");
  // Vergleich der beiden Objekte
  HoldValue val = (HoldValue)obj;
  return this.intVar.CompareTo(val.intVar);
}

Generell sollten Sie die Methode CompareTo der Schnittstelle IComparable wie gezeigt implementieren, um gegen alle unzulässigen Aufrufe gewappnet zu sein. Es wird zuerst überprüft, ob dem Parameter null übergeben wurde. Der Vergleich sollte daraufhin abgebrochen werden und als Resultat einen Wert größer 0 liefern. Damit wird ein null-Verweis vor einem Objektverweis einsortiert. Unterscheiden sich die beiden Typen des anstehenden Vergleichs, wird die Ausnahme ArgumentException ausgelöst und muss vom Aufrufer behandelt werden.

Auf der Buch-CD finden Sie den Programmcode des Beispiels unter:

...\Kapitel 7\IComparableDemo

Vergleichsklassen mit »IComparer«

Das Sortieren einer ArrayList mit der parameterlosen Sort-Methode gestattet nur ein Vergleichskriterium. Manchmal ist aber erforderlich, unterschiedliche Sortierkriterien zu berücksichtigen. Nehmen wir zum Beispiel die Klasse Person, welche die beiden Felder Name und Wohnort beschreibt.


class Person {
  public string Name;
  public string Wohnort;
  public Person(string name, string ort) {
    Name = name;
    Wohnort = ort;
  }
}

Würden die Klasse die Schnittstelle IComparable implementieren, müsste die Entscheidung getroffen werden, nach welchem Feld Objekte dieser Klasse sortiert werden können. Nun sollen beide Möglichkeiten angeboten werden.

Die Lösung des Problems führt über die Bereitstellung so genannter Vergleichsklassen, welche die Schnittstelle IComparer implementieren. In jeder Vergleichsklasse wird genau ein Vergleichskriterium festgelegt. Wollen wir einen bestimmten Objektvergleich erzwingen, müssen wir der Sort-Methode mitteilen, welche Vergleichsklasse dafür bestimmt ist. Dafür stehen uns zwei Überladungen zur Verfügung, denen die Referenz auf ein Objekt übergeben wird, das die Schnittstelle IComparer implementiert:


public virtual void Sort(IComparer);
public virtual void Sort(int, int, IComparer);

Mit der Überladung, die zwei int erwartet, können der Startindex und die Länge des zu sortierenden Bereichs bestimmt werden. Bei sehr großen Auflistungen steigert das die Performance, da Sortiervorgänge sehr rechenintensiv sind.

Die Schnittstelle IComparer stellt eine Methode für den Vergleich zweier Objekte bereit:


int Compare(object x, object y);

Compare funktioniert ähnlich der weiter oben erörterten Methode CompareTo und gibt die folgenden Werte zurück:

gp  < 0, wenn das erste Objekt »kleiner« als das zweite Objekt ist.
gp  0, wenn das erste Objekt »gleich« dem zweiten Objekt ist.
gp  > 0, wenn das erste Objekt »größer« als das zweite Objekt ist.

Hinweis   Der große Unterschied zwischen den beiden Schnittstellenmethoden IComparable. CompareTo und IComparer.Compare ist die Parameterliste, und der daraus resultierende Methodenaufruf. CompareTo nimmt eine Referenz entgegen, die mit dem aktuellen Objekt verglichen wird, während Compare zwei zu vergleichende Referenzen übergeben werden. Somit ist diese Methode auch unabhängig von der this-Referenz. Die Schnittstelle IComparer bietet sich daher auch an, wenn Sie die Objekte eines Typs vergleichen wollen, der nicht IComparable implementiert.

Für die Klasse Person wollen wir nun die beiden Vergleichsklassen NameComparer und WohnortComparer entwickeln, die gemäß Forderung die Schnittstelle IComparer implementieren und nach Wohnort bzw. Name sortieren.


// Vergleichsklasse – Kriterium 'Wohnort'
class WohnortComparer : IComparer {
  public int Compare(object x, object y) {
    // prüfen auf null-Übergabe
    if(x == null && y == null) return 0;
    if(x == null) return 1;
    if(y == null) return –1;
    // Typüberprüfung
    if(x.GetType() != y.GetType())
      throw new ArgumentException("Ungültiger Vergleich");
    // Vergleich
    return ((Person)x).Wohnort.CompareTo(((Person)y).Wohnort);
  }
}
// Vergleichsklasse – Kriterium 'Name'
class NameComparer : IComparer {
  public int Compare(object x, object y) {
    // prüfen auf null-Übergabe
    if(x == null && y == null) return 0;
    if(x == null) return 1;
    if(y == null) return –1;
    // Typüberprüfung
    if(x.GetType() != y.GetType())
      throw new ArgumentException("Ungültiger Vergleich");
    // Vergleich
    return ((Person)x).Name.CompareTo(((Person)y).Name);
  }
}

Die Implementierung ähnelt der der Methode CompareTo. Zuerst sollte wieder ein Vergleich mit null durchgeführt werden und anschließend eine Prüfung, ob beide Parameter denselben Typ beschreiben oder zumindest einen vergleichbaren Typ besitzen. Sollte keine Bedingung zutreffen, kann der Vergleich der Objekte erfolgen. Dabei unterstützt uns die Klasse String, die ihrerseits die IComparable-Schnittstelle implementiert, mit der Methode CompareTo.

Haben wir ein ArrayList-Objekt mit Person-Objekten gefüllt, steht es uns frei, welche Vergleichsklasse wir zur Sortierung der Objekte benutzen, denn beide sind auf dieselbe Schnittstelle zurückzuführen und gegenseitig austauschbar.


// -----------------------------------------------------------
// Beispiel: ...\Kapitel 7\IComparerDemo
// -----------------------------------------------------------
class Program {
  static void Main(string[] args) {
    ArrayList arrList = new ArrayList();
    // ArrayList füllen
    Person pers1 = new Person("Meier", "Berlin");
    arrList.Add(pers1);
    Person pers2 = new Person("Arnhold", "Köln");
    arrList.Add(pers2);
    Person pers3 = new Person("Graubär", "Aachen");
    arrList.Add(pers3);
    // nach Wohnorten sortieren
    arrList.Sort(new WohnortComparer());
    Console.WriteLine("Liste nach Wohnorten sortiert");
    ShowSortedList(arrList);
    // nach Namen sortieren
    arrList.Sort(new NameComparer());
    Console.WriteLine("Liste nach Namen sortiert");
    ShowSortedList(arrList);
  }
  static void ShowSortedList(IList liste) {
    foreach(Person temp in liste) {
      Console.Write("Name = {0,-12}", temp.Name);
      Console.WriteLine("Wohnort = {0}", temp.Wohnort);
    }
    Console.WriteLine();
  }
}

Die statische Methode »ArrayList.Adapter«

Mit der statischen Methode Adapter kann ein Wrapper (darunter ist eine Klasse zu verstehen, die sich um eine andere legt) um ein IList-Objekt gelegt werden. Der Rückgabewert ist die Referenz auf ein neues ArrayList-Objekt, auf dessen Methoden sich das IList-Objekt manipulieren lässt.


public static ArrayList Adapter(IList list);

Wie Sie die Methode Adapter einsetzen können, möchte ich Ihnen an einem Beispiel zeigen. Wie Sie der .NET-Dokumentation zu IList entnehmen können, implementiert auch ein gewöhnliches Array diese Schnittstelle. Ein Array kann allerdings nicht sortiert werden. Über den Aufruf von Adapter wird das allerdings möglich.

Nehmen wir an, die Klasse Person sei wie folgt definiert:


public class Person {
  public string Name;
  public int Alter;
  public Person(string name, int alter) {
    Name = name;
    Alter = alter;
  }
}

Ein Array vom Typ Person soll mehrere Objekte enthalten, die dem Namen nach sortiert werden sollen. Dazu rufen wir die statische Methode Adapter unter Übergabe des Arrays auf und weisen die zurückgelieferte Referenz der Methode einer ArrayList-Variablen zu.


Person[] pers = new Person[3];
pers[0] = new Person("Peter", 15);
pers[1] = new Person("Alfred", 33);
pers[2] = new Person("Hugo", 26);
ArrayList liste = ArrayList.Adapter(pers);

Die Elemente des Arrays sollen jetzt dem nach Namen sortiert werden. Dazu bietet sich eine Überladung der Methode Sort der ArrayList an:


public virtual void Sort(IComparer);

Da ein Array die Schnittstelle IComparer nicht implementiert, die notwendig wäre, um diese Überladung aufzurufen, müssen wir eine Vergleichsklasse, welche die Schnittstelle IComparer implementiert, bereitstellen:


public class SortByName : IComparer {
  public int Compare(object x, object y) {
    return ((Person)x).Name.CompareTo(((Person)y).Name);
  }
}

Mit dem Aufruf von Sort unter Übergabe eines Objekts vom Typ der Vergleichsklasse SortByName können wir über den bereitgestellten Wrapper die Elemente des Arrays pers in die gewünschte Reihenfolge bringen:


liste.Sort(new SortByName());

Den vollständigen Programmcode zu diesem Beispiel finden Sie auf der Buch-CD unter:

.\Kapitel 7\ArrayList_Adapter)


Galileo Computing

7.3.4 Die Schnittstelle »IDictionary«  downtop

IList-Auflistungen verwalten die Objekten über Indizes. Dieses Konzept hat aber einen Nachteil: Wenn man nach einem bestimmten Element sucht und dessen Position nicht kennt, muss man die Liste so lange durchlaufen, bis man eine Übereinstimmung findet. Enthält die Auflistung sehr viele Einträge, kann das sehr zeitaufwändig sein und kostet Rechenleistung.

Kommt es nicht auf die Reihenfolge der Elemente an, kann man sich für eine Auflistung, die das Interface IDictionary implementiert, entscheiden. Dazu gehört die Klasse Hashtable, die unten vorgestellt wird. In diesen Auflistungen kann ein bestimmtes Element zwar schneller gefunden werden, allerdings muss man dabei in Kauf, keinen Einfluss auf die Positionierung der Elemente in der Liste zu haben. IDictionary-Collections organisieren die Elemente in einer für sie passenden Reihenfolge.

Um nach einem Element in einer IDictionary-Auflistung zu suchen, wird eine Schlüsselinformation benötigt, der ein Wert zugeordnet ist. IDictionary-Auflistungen enthalten Elemente mit Schlüssel-Wert-Kombinationen. Der Schlüssel muss eindeutig sein und darf nicht den Inhalt null haben. In einer IList-Collection entspricht der Schlüssel dem Index. Der wesentliche Unterschied ist dabei jedoch, dass der Schlüssel nicht garantiert, eindeutig einem bestimmten Eintrag zugeordnet zu sein.

Stellen Sie sich dazu vor, Sie beabsichtigten, die Mitarbeiter eines Unternehmens in einer Auflistung zu verwalten. Jeder Mitarbeiter ist über eine eindeutige Personalnummer identifizierbar. Diese Personalnummer beschreibt gleichzeitig, welche persönlichen Daten zu dem Mitarbeiter gehören:


0999–123–3 = Franz Fischer
0100–288–3 = Peter Müller
6771–771–1 = Marita Kohl

Hinter jeder Personalnummer verbirgt sich genau ein Mitarbeiter, aber einem Mitarbeiter könnten durchaus auch zwei Personalnummern zugewiesen werden – vielleicht weil er zwei separat honorierte Positionen besetzt. Diese drei Wertpaare ließen sich problemlos durch eine IDictionary-Auflistung abbilden. Der Schlüssel würde durch die Personalnummer beschrieben, der Name entspräche dem Wert. In der Realität würde man dann allerdings wahrscheinlich sinnvollerweise den Namen durch eine Objektreferenz ersetzen, die auf das dem Mitarbeiter zugeordnete Objekt verweist. Der Schlüssel kann durchaus selbst ein Objekt sein, wird aber häufig durch Zeichenfolgen beschrieben.

Methoden und Eigenschaften der Schnittstelle »IDictionary«

Die meisten der von IDictionary veröffentlichten Methoden sind uns bereits aus der Schnittstelle IList bekannt. Das erleichtert zwar einerseits die Einarbeitung, zwingt uns aber andererseits dennoch in einigen Fällen zu einer etwas genaueren Betrachtung.

Jeder Listeneintrag in einer IDictionary-Auflistung wird durch ein Schlüssel-Wert-Paar beschrieben, was sich in der Parameterliste der Add-Methode niederschlägt:


void Add(object key, object value);

Der erste Parameter wird als Schlüssel für das hinzuzufügende Element verwendet und sorgt für die Identifizierbarkeit innerhalb einer Liste, der zweite ist die Referenz auf das hinzuzufügende Element. Wir stoßen hier zum ersten Mal auf die Tatsache, dass von IDictionary-Auflistungen anstelle eines Index ein Schlüssel verwendet wird.

Der Schlüssel begleitet uns durch alle Methoden und wird auch von Remove zum Entfernen eines Objekts aus der Auflistung verwendet:


void Remove(object key);

Da IDictionary-Objekte nicht über Indizes verwaltet werden, brauchen nach dem Löschen eines Elements etwaige Folgeelemente auch keine Lücke zu schließen.

Dem Indexer kommt nicht nur die Aufgabe zu, unter der Angabe des Schlüssels den Zugriff auf das gewünschte Element zu gewährleisten, vielmehr kann er auch dazu benutzt werden, den Wert eines Objekts zu verändern.


object this[object key] {get; set;}

Gibt man einen Schlüssel an, der sich noch nicht in der Auflistung befindet, wird das Element hinzugefügt. Dabei bleibt der Wert leer, ist also null, was durchaus zulässig ist.

Die Schlüssel und die Werte werden in eigenen Auflistungen verwaltet. Die Referenz auf diese internen Auflistungen liefern die Eigenschaften Keys und Values.


ICollection Keys {get;}
ICollection Values {get;}

Mit Clear kann eine IDictionary-Auflistung geleert werden, mit Contains können wir prüfen, ob ein bestimmter Schlüssel bereits in der Liste enthalten ist.


Tabelle 7.4   Eigenschaften und Methoden des Interface »IDictionary«

Eigenschaft/Methode Beschreibung
Add Hinzufügen eines Objekts zur Auflistung.
Remove Löschen eines Elements aus der Auflistung.
Item Zugriff auf ein Element der Auflistung.
Keys Liefert alle in der Liste verwendeten Schlüssel zurück.
Values Liefert alle in der Liste verwendeten Werte zurück.
Clear Löscht alle Elemente der Auflistung.
Contains Prüft, ob ein bestimmter Schlüssel in der Auflistung enthalten ist.


Galileo Computing

7.3.5 Die Klasse »Hashtable«  downtop

Die wichtigste Auflistung, die das IDictionary-Interface implementiert, wird von der Klasse Hashtable beschrieben. Dieser Auflistungstyp ist eine Datenstruktur, die ein schnelles Suchen nach Objekten erlaubt. Der Name rührt daher, dass für die Verwaltung der Elemente ein Hashwert für den Schlüssel verwendet wird. Zum Erzeugen des Hashwerts wird intern die von Object geerbte Methode GetHashCode ausgeführt.

Im folgenden Beispiel wird eine Hashtabelle erzeugt, die vier Objekte vom Typ ClassA sowie eine Zeichenfolge verwaltet. Damit wir sehen, wie wir über den Tabelleneintrag auf das Member eines registrierten Elements zugreifen, ist in der Definition der ClassA die öffentliche Eigenschaft intVar deklariert, der wir über den Konstruktor einen Wert übergeben. Im Beispielcode werden die wichtigsten Methoden einer Hashtabelle benutzt, um Informationen sowohl über die Elemente als auch über die Listeneinträge zu erhalten.


// -------------------------------------------------------------
// Beispiel: ...\Kapitel 7\Hashtabelle
// -------------------------------------------------------------
class Program {
  static ClassA obj1 = new ClassA(1);
  static ClassA obj2 = new ClassA(2);
  static ClassA obj3 = new ClassA(3);
  static ClassA obj4 = new ClassA(4);
  static Hashtable myHash;
  static void Main(string[] args) { 
    myHash = new Hashtable();
    // Objekte der Hashtabelle hinzufügen
    AddObjects();
    // Liste der Schlüssel ausgeben
    GetKeyList();
    // Liste der Werte ausgeben
    GetValueList();
    // Liste der Schlüssel und Werte ausgeben
    GetCompleteList();
    Console.WriteLine();
    // Zugriff auf ein bestimmtes Element
    Console.Write("Geben Sie nun den Schlüssel ");
    Console.Write("des Objekts ein, dessen Eigenschaft ");
    Console.Write("intVar Sie auswerten wollen: ");
    string input = Console.ReadLine();
    // prüfen, ob der Schlüssel sich in der Hashtabelle befindet
    if(myHash.Contains(input)) {
      Console.Write("Das Objekt {0} ", input);
      Console.Write("hat in intVar den Inhalt {0}", 
                             (ClassA)myHash[input]).intVar);
      Console.WriteLine();
    }
    else {
      Console.WriteLine("Nicht Element der Hashtabelle");
    }
    // anhand des Wertes prüfen, ob sich ein Objekt 
    // bereits in der Hashtabelle befindet
    Console.Write("Aufruf von ContainsValue: ");
    if(myHash.ContainsValue(obj2))
      Console.WriteLine("Das Objekt ist enthalten.");
    else
      Console.WriteLine("Das Objekt ist enthalten.");
    Console.ReadLine();
  }
  // Ausgabe der Werteliste
  public static void GetValueList() {
    Console.WriteLine();
    Console.WriteLine("===== Werteliste =====");
    foreach(object obj in myHash.Values)
      Console.WriteLine(obj);
  }
  // Ausgabe der Schlüsselliste
  public static void GetKeyList() {
    Console.WriteLine();
    Console.WriteLine("===== Schlüsselliste =====");
    foreach(object obj in myHash.Keys)
      Console.WriteLine(obj);
  }
  // Schlüssel-Wert-Paar über ein DictionaryEntry-Objekt ausgeben
  public static void GetCompleteList() {
    Console.WriteLine();
    Console.WriteLine("===== Schlüssel-/Wertepaare =====");
    foreach(DictionaryEntry dicEntry in myHash) {
      Console.Write(dicEntry.Key);
      Console.WriteLine(" – {0}", dicEntry.Value);
    }
  }
  //  Objekte der Hashtabelle hinzufügen
  public static void AddObjects(){
    myHash.Add("eins", obj1);
    myHash.Add("zwei", obj2);
    myHash.Add("drei", obj3);
    myHash.Add("vier", obj4);
    myHash.Add("fünf", "Hallo");
  }
}
class ClassA {
  public int intVar;
  public ClassA(int x) {
    intVar = x;}
}

Das Objekt myHash vom Typ Hashtable wird mit dem parameterlosen Konstruktor erzeugt, der meistens ausreichen dürfte. Anschließend werden in der benutzerdefinierten Methode AddObjects fünf ClassA-Objekte in der Hashtabelle registriert. Dem Aufruf der Methode Add werden dazu Schlüssel und Wert übergeben. Im Beispiel ist der Schlüssel eine Zeichenfolge, die Objektreferenz stellt den Wert dar. Ist ein Schlüssel bereits in der Hashtabelle enthalten, kommt es zur Auslösung der Ausnahme ArgumentException.

»DictionaryEntry« zur Auswertung der Schlüssel oder Werte

Der Inhalt einer HashTable lässt sich abfragen – sowohl die Liste der Schlüssel als auch die Liste der Werte. Dazu dienen die Eigenschaften Keys und Values. In den Methoden GetKeyList und GetValueList wird in jeweils einer foreach-Schleife die Werte- bzw. Schlüsselliste durchlaufen. Beachten Sie, dass die Laufvariablen der Schleifen nicht dazu benutzt werden können, auf das Listenelement zuzugreifen, um in unserem Fall beispielsweise das Feld intVar auszuwerten. Daher wird auch zur Laufzeit eine Ausnahme ausgelöst, wenn Sie versuchen, die Laufvariable mit


// Vorsicht: Falsche Konvertierung
foreach(object obj in myHash)
  Console.WriteLine(((ClassA)obj).intVar);

oder mit


// Vorsicht: Falsche Konvertierung
foreach(object obj in myHash)
  Console.WriteLine(((ClassA)myHash[obj]).intVar);

zu konvertieren.

Um auf ein Listenelement in einer foreach-Schleife zugreifen zu können, können Sie die Laufvariable vom Typ DictionaryEntry deklarieren. Tatsächlich sind die Elemente in einer HashTable von diesem Typ. DictionaryEntry ist eine Struktur, die das Schlüssel-Wert-Paar für einen Hashtabelleneintrag enthält. Über die Eigenschaften Key und Value können wir die notwendigen Informationen beziehen. Während uns Key nur den Schlüssel liefert, können wir über den Rückgabewert von Value nach vorheriger Typumwandlung auf das Objekt zugreifen:


foreach(DictionaryEntry dicEntry in myHash) {
   ...
   Console.WriteLine(((ClassA)dicEntry.Value).intVar);
}


Hinweis   Dass die Einträge in einer Hashtabelle vom Typ DictionaryEntry sind, müssen Sie berücksichtigen, wenn Sie mit der Methode CopyTo die Einträge in ein Array kopieren wollen. Das Array muss dann vom diesem Typ oder vom Typ object sein.

Prüfen, ob ein Element bereits zur »Hashtable« gehört

Eine HashTable dient zur Verwaltung mehrerer meist gleichartiger Objekte und hat im Vergleich zu anderen Auflistungen den Vorteil, einen sehr schnellen Zugriff über den Indexer zu ermöglichen. Im Beispiel oben wird der Benutzer an der Konsole dazu aufgefordert, einen Schlüssel anzugeben, nach dem in der Hashtabelle gesucht werden soll. Ob der Schlüssel einem Element der Auflistung zugeordnet werden kann, wird durch die Methode Contains festgestellt, die einen booleschen Wert zurückliefert:


if(myHash.Contains(input))...

Analog könnte man auch die Methode ContainsKey benutzen, die sich in keiner Weise von Contains unterscheidet.

Nicht nur über den Schlüssel lässt sich prüfen, ob ein Element Mitglied der Hashtabelle ist. Auch über den booleschen Rückgabewert von ContainsValue ist das möglich. Im Beispiel wird dazu direkt die Referenz obj2 übergeben, die natürlich immer zu derselben Konsolenausgabe führt:


if(myHash.ContainsValue(obj2))...


Galileo Computing

7.3.6 Die Klassen »Queue« und »Stack«  downtop

Weder die Klasse Queue noch die Klasse Stack implementiert das Interface IList oder IDictionary. Dennoch werden beide den Auflistungen zugerechnet, weil sie die Schnittstellen ICollection und somit auch IEnumerable implementieren.

Stack ist eine Datenstruktur, die nach dem LIFO-Prinzip (Last-In-First Out) arbeitet: Das Element, das als letztes eingefügt wurde, wird beim folgenden Lesevorgang wieder entnommen. Daraus folgt, dass man auf das Element, das als erstes auf den Stack gelegt worden ist, erst dann wieder zugreifen kann, wenn alle anderen Elemente den Stack verlassen haben.

Ein Queue-Objekt ist das Pendant zu Stack. Es arbeitet nach dem FIFO-Prinzip (First-In-First Out), das besagt, dass das zuerst in die Queue geschobene Element auch als erstes wieder entnommen wird. Das Prinzip gleicht also einer Warteschlange an der Kasse eines Fußballstadions.

Die »Stack«-Klasse

Schauen wir uns an einem Beispiel an, wie man mit der Klasse Stack arbeitet.


// -------------------------------------------------------------
// Beispiel: ...\Kapitel 7\StackClass
// -------------------------------------------------------------
class Program {
  static void Main(string[] args) {
    Stack myStack = new Stack(11);
    // Stack füllen
    for(int i = 0; i <= 10; i++)
      myStack.Push(i * i);     
    // Ausgabe an der Konsole
    PrintStack(myStack);
    Console.ReadLine();
  }
  public static void PrintStack(Stack obj) {
    // alle Elemente aus dem Stack holen
    while(obj.Count != 0) {
      Console.WriteLine(obj.Pop());
    }
  }
}

Das Hinzufügen neuer Elemente geschieht durch den Aufruf der Methode Push, die als Argument ein Objekt erwartet. Im Beispielcode wird eine Schleife durchlaufen, in der insgesamt elf Zahlen auf den Stack gelegt werden. Es handelt sich dabei immer um das Quadrat des aktuellen Schleifenzählers.

Zugegriffen werden kann nur auf das oberste Element im Stack. Dabei handelt es sich immer um das Objekt, das als letztes mit der Push-Methode auf den Stack gelegt wurde.

Es bieten sich zwei Alternativen an, das oberste Element auszuwerten: Mit Pop wird das oberste Element nicht nur zurückgeliefert, sondern gleichzeitig auch der Stack-Verwaltung entzogen. Mit Peek erhält man zwar die Referenz, ohne es jedoch gleichzeitig zu entfernen. Im Beispiel wird der Stack so lange mit Pop abgegriffen, bis die Liste wieder leer ist. Die Reihenfolge der Zahlen beim Hinzufügen lautete:


0 1 4 9 16 25 36 ... 81 100

Die Rückgabe erfolgt mit:


100 81 64 ... 25 16 9 4 1 0

Der Aufruf des parameterlosen Konstruktors der Klasse Stack führt zu einer Standardkapazität von 32 Elementen, die bei Bedarf automatisch erhöht wird, um weitere Elemente aufzunehmen. Dabei werden alle Elemente in ein neues Array kopiert. Wenn Sie wissen, dass Sie diese Anzahl überschreiten werden, sollten Sie aus Gründen einer besseren Performance den parametrisierten Konstruktor wählen, der die Übergabe der erforderlichen Startkapazität ermöglicht:


Stack stack = new Stack(100);

Reicht das immer noch nicht aus und wird zur Laufzeit die Initialisierungsgröße trotzdem überschritten, verdoppelt sich die Kapazität automatisch.

Die Klasse »Queue«

Das Beispiel, das vorhin die Klasse Stack veranschaulichte, wird nun auf ein Queue-Objekt umgeschrieben:

 


// -------------------------------------------------------------
// Beispiel: ...\Kapitel 7\QueueClass
// -------------------------------------------------------------
class Program {
  static void Main(string[] args) {
    Queue myQueue = new Queue();
    // Queue füllen
    for(int i = 0; i <= 10; i++)
      myQueue.Enqueue(i * i);
    // Ausgabe an der Konsole
    PrintStack(myQueue);
    Console.ReadLine();
  }
  public static void PrintStack(Queue obj) {
    // alle Elemente aus dem Stack holen
    while(obj.Count != 0) {
      Console.WriteLine(obj.Dequeue());
    }
  }
}

Diesmal sind es die beiden Methoden Enqueue und Dequeue, mit denen Elemente in die Liste geschoben und wieder aus ihr geholt werden. Dequeue liefert nicht nur die Referenz des sich am Anfang befindlichen Elements, es holt dieses Element auch aus der Warteschlange. Wie bei der Klasse Stack können Sie sich mit Peek auch die Referenz dieses Elements besorgen und es gleichzeitig in der Liste lassen.

Die Elementzugriff erfolgt in derselben Reihenfolge, in der die Objekte der Liste hinzugefügt wurden: Das erste hinzugefügte Element wird auch als erstes herausgeholt, danach kann man das zweite in die Warteschlange gelegte holen usw. Ein Zugriff auf ein beliebiges Element ist weder beim Stack noch bei der Queue möglich.

Die Standardkapazität eines Queue-Objekts beträgt 32 Elemente, die Sie mittels eines anderen Konstruktors bei der Instanziierung bedarfsgerecht festlegen können.


Galileo Computing

7.3.7 Objektauflistungen im Überblick  downtop

Mit ArrayList, Hashtable, Queue und Stack haben Sie bereits die wichtigsten Auflistungsklassen kennen gelernt. Die .NET-Klassenbibliothek stellt darüber hinaus noch weitere, auf spezifische Anwendungsfälle optimierte Auflistungen bereit, von denen die meisten im Namespace System.Collections.Specialized zu finden sind.


Hinweis   Genau genommen unterschlage ich Ihnen an dieser Stelle eine ganz neue Gruppe von Auflistungen, die seit dem .NET Framework 2.0 verfügbar sind. Denn die Auflistungen unterteilen sich in zwei Gruppen: untypisierte Auflistungen typisierte Auflistungen (generische Auflistungen)
Generische Auflistungen wurden mit .NET Framework 2.0 eingeführt und bieten gegenüber den untypisierten den Vorteil, dass sie bereits zur Entwicklungszeit auf einen bestimmten Typ geprägt werden können, d. h., es wird ein ganz bestimmter Typ verwaltet. Generische Auflistungen finden Sie im Namespace System.Collections.Generic. Mit generischen Typen befassen wir uns im nächsten Abschnitt.

In der folgenden Tabelle erhalten Sie einen Überblick über die Auflistungsklassen, mit denen wir uns nicht näher beschäftigt haben. Da wir uns bereits einige typische Auflistungen genauer angesehen haben, ist es sicherlich nicht mehr schwierig, sich im Bedarfsfall in die Fähigkeiten einer anderen einzuarbeiten. Letztendlich finden wir immer die gleichen Eigenschaften und Methoden vor, die sich meist nur in der Parameterliste unterscheiden.


Tabelle 7.5   Weitere Auflistungsklassen des .NET Frameworks

Klasse Beschreibung
BitArray Verwaltet einen Array von Bits.
CollectionsUtil Eine Auflistung, bei der keine Unterscheidung zwischen Groß- und Kleinschreibung erfolgt.
HybridDictionary Das Verhalten orientiert sich an der Anzahl der Listenelemente. Ist die Anzahl der Elemente gering, operiert diese Klasse als ListDictionary-Collection, wird die Anzahl größer, als Hashtable.
ListDictionary Solange die Anzahl der Elemente kleiner zehn ist, werden die Operationen mit den Elementen schneller ausgeführt als bei einer Hashtable.
NameValueCollection Verwaltet ein Schlüssel-Wert-Paar, wobei sowohl der Schlüssel als auch der Wert durch Zeichenfolgen beschrieben werden. Einem Schlüssel können mehrere Zeichenfolgen zugeordnet werden, d. h., der Schlüssel ist nicht eindeutig.
SortedList Diese Auflistung verwaltet Schlüssel-Wert-Paare, die nach den Schlüsseln sortiert sind und auf die sowohl über Schlüssel als auch über Indizes zugegriffen werden kann. Damit vereint sie die Merkmale von Hashtable und ArrayList.
StringCollection Eine Auflistung, die nur Zeichenfolgen enthält
StringDictionary Ähnlich einer Hashtable, der Schlüssel ist jedoch immer eine Zeichenfolge

Welche Auflistung entspricht meinen Anforderungen?

Im Einzelfall kann es sich als schwierig erweisen, aus der großen Anzahl der angebotenen Typen die für die aktuellen Anforderungen am besten geeignete zu wählen. Im Ausschlussverfahren sollten Sie sich dem Typ nähern, der Ihnen unter den gegebenen Umständen die maximale Performance und das gewünschte Verhalten bietet.

Wissen Sie, dass der wahlfreie, also beliebige Zugriff auf die Listenelemente erforderlich ist, verabschieden sich bereits die ersten beiden Klassen aus dem Angebot (Stack und Queue). Das nächste Kriterium auf dem Weg zur Entscheidungsfindung dürfte die Antwort auf die Frage sein, ob die Verwaltung über einen Index gewünscht oder sogar gefordert wird. Das könnte beispielsweise der Fall sein, wenn in einer Schleife über einen Schleifenzähler die Listenelemente der Reihe nach besucht werden müssen. Die Wahl würde in diesem Fall ArrayList oder SortedList lauten.

Objekte, die sich durch eine Schlüssel-Wert-Kombination beschreiben lassen, werden meist in Auflistungen verwaltet, die nicht indexbasiert sind. Ist die Anzahl der Elemente sehr klein, würde sich der Typ ListDictionary anbieten, ist die Anzahl größer, eignet sich besser Hashtable. Falls Sie keinen Mut zur Entscheidung haben – mit HybridDictionary geben Sie die Verantwortung ab. Liegt eine Schlüssel-Wert-Kombination vor und können Sie dennoch nicht auf die Elementsortierung in der Liste verzichten, lautet die Entscheidung wieder SortedList.

In speziellen Sonderfällen wird man auch noch einen Blick auf andere Typen werfen müssen, aber mit den eben erwähnten sind sicherlich 95  % aller Anwendungsfälle abzudecken.


Galileo Computing

7.3.8 Benutzerdefinierte Auflistungen  toptop

Sie suchen eine Auflistungsklasse, die ausschließlich bestimmte Typen verwaltet? Möglicherweise sogar Objekte eines benutzerdefinierten Typs? Sie werden mit Sicherheit keine passende Klasse mit der geforderten strikten Typbindung im .NET Framework finden. Sie haben jetzt zwei Alternativen:

1.  Sie leiten eine passende, vom .NET Framework zur Verfügung gestellte Klasse ab, z.B. CollectionBase.
2. Sie entwickeln eine generische Auflistungsklasse oder benutzen eine aus der .NET-Klassenbibliothek.
       

Ich werden Ihnen zuerst zeigen, wie Sie eine eigene Auflistungsklasse durch Ableitung bereitstellen. Im Abschnitt 7.4 werden wir uns mit den Generics auseinander setzen, die einen anderen, sicherlich auch einfacheren Weg aufzeigen. Nichtsdestotrotz halte ich es für sehr lehrreich, sich den Weg über die Ableitung anzusehen. Lassen Sie uns deshalb damit starten.

Die Ableitung der Klasse »CollectionBase«

Angenommen wir hätten eine Klasse namens HoldValue entwickelt und wollen viele Objekte dieses Typs von einer Auflistung verwalten lassen. Ein erster Ansatz könnte sein, eine neue Klasse bereitzustellen, die ein Objekt vom Typ ArrayList aggregiert. Von unserer Auflistungsklasse werden Methoden und Eigenschaften veröffentlicht, die es ermöglichen, durch Weiterleitung die Methoden und Eigenschaften des internen ArrayList-Objekts zu bedienen.


class HoldValue {
  public int Value;
  public HoldValue(int value) {
    Value = value;
  }
}
// benutzerdefinierte Klasse, die wie eine Auflistung agiert
class HoldValueList {
  // Private Instanz der Klasse ArrayList
  private ArrayList col;
  // Konstruktor
  public HoldValueList() {
    col = new ArrayList();
  }   
  // Hinzufügen eines HoldValue-Objekts
  public void Append(HoldValue obj) {
    col.Add(obj);
  }
  // Löschen eines verwalteten HoldValue-Objekts
  public void Delete(int index) {
    col.RemoveAt(index);
  }  
  ...
}

Einem Umstand hatten wir dabei keine Aufmerksamkeit geschenkt: Auflistungen sind auch dadurch charakterisiert, dass auf ihre Elemente in einer foreach-Schleife der Reihe nach zugegriffen werden kann. Wie wir inzwischen wissen, setzt das voraus, dass die Auflistungsklasse die Schnittstelle IEnumerable mit ihrer Methode GetEnumerator implementiert. Davon ist HoldValueList aber weit entfernt.

Den Enumerator zu implementieren ist verhältnismäßig komplex. Um uns die Programmierung einfacher zu machen, stellt uns die .NET-Klassenbibliothek mit CollectionBase, ReadOnlyCollectionBase und DictionaryBase drei abstrakte Klassendefinitionen zur Verfügung, die wir für unsere Zwecke optimal nutzen können.

Am Beispiel von CollectionBase wollen nun die Klasse HoldValueList so implementieren, dass sie alle an eine Collection gestellten Anforderungen erfüllen kann. Sehen wir uns aber zuerst die Definition der Klasse an:


public abstract class CollectionBase : IList, ICollection, IEnumerable

CollectionBase implementiert alle erforderlichen Schnittstellen und ist abstrakt definiert, muss also abgeleitet werden. Die Schnittstelle IList deutet bereits an, dass sich ein Objekt vom Typ CollectionBase ähnlich wie eine ArrayList verhalten wird, die Elemente also indexbasiert verwaltet.

Werfen wir nun einen Blick auf die Mitglieder der Klasse in der Online-Dokumentation. Im ersten Moment mag die Länge der Liste schockieren. Wenn Sie sich etwas genauer damit beschäftigen, werden Sie erkennen, wie trick- und folglich auch für uns lehrreich die Implementierung ist und wie weit sie in die Tiefen der objektorientierten Programmierung eindringt.

Fangen wir mit dem Block der »Geschützten Eigenschaften« an. Hier können wir erkennen, dass auch CollectionBase das Rad nicht neu erfindet, sondern ein ArrayList-Objekt aggregiert, auf das die Eigenschaften InnerList die Referenz liefert:


protected ArrayList InnerList {get;}

Es gibt mit List noch eine zweite geschützte Eigenschaft. Diese liefert die Referenz auf die IList-Schnittstelle der CollecionBase.


protected IList List {get;}

Den Unterschied zwischen diesen beiden Referenzen und die daraus resultierenden Konsequenzen werde ich später erklären.

Unter »Öffentliche Eigenschaften« und »Öffentliche Methoden« finden wir neben den von object geerbten Membern auch Count, Clear und RemoveAt wieder. Diese sind unabhängig vom verwalteten Typ und bedürfen daher keiner besonderen Aufmerksamkeit.

Die Klasse CollectionBase ist bereits vollständig implementiert. Die Kennzeichnung mit abstract zwingt nur dazu, die Klasse abzuleiten, um sie zu typisieren. Alle Methoden, die von den Schnittstellen übernommen werden, funktionieren jedoch bereits tadellos.

CollectionBase implementiert die Schnittstelle IList und muss daher eine Add-Methode haben. Diese wird jedoch nicht veröffentlicht, sondern explizit implementiert, was einer »Privatisierung« gleichkommt. Damit hat man über eine Referenz der Klasse HoldValueList keinen direkten Zugriff auf die Add-Methode, sondern nur über eine IList-Referenz:


HoldValueList liste = new HoldValueList();
IList ilist = (IList)liste;
ilist.Add(new HoldValue(3));
Console.WriteLine(liste.Count);

Tatsächlich wird dieser Code an der Konsole die Zahl 1 ausgeben, das Objekt wurde also hinzugefügt.

Bis auf Clear, RemoveAt und Count implementiert CollectionBase alle Methoden von IList und ICollection explizit (siehe dazu auch im Block »Explizite Schnittstellenimplementierung« in der .NET-Dokumentation). Auch wenn wir jetzt wissen, wie wir unsere Klasse HoldValueList austricksen können, die Forderung, nur HoldValue-Objekte zu verwalten, erfüllt sie noch nicht.

Exemplarisch für alle anderen Methoden wollen wir daher jetzt die Add-Methode in HoldValueList implementieren, damit auf eine HoldValueList-Referenz die übliche Methode zum Hinzufügen eines Eintrags aufgerufen werden kann. Der Parameter wird nun spezialisiert, er ist vom Typ HoldValue.


class HoldValueList : CollectionBase {
  public void Add(HoldValue obj) {
    this.InnerList.Add(obj);
  }
}

Die übergebene Referenz wird in die aggregierte Auflistung, deren Referenz InnerList liefert, eingetragen. Beachten Sie, dass hier nicht List verwendet wird. Damit ist sichergestellt, dass wir der benutzerdefinierten Auflistung HoldValueList nur einen bestimmten Typ übergeben können – oder etwa nicht? Nein, denn weiterhin kann die Add-Methode mit


IList ilist = (IList)liste;
ilist.Add(new ClassA(3));